理解 ECMAScript 规范-part2

原文: https://v8.dev/blog/understanding-ecmascript-part-2

了解规范的一个有趣方法是从我们已知的 JavaScript 功能开始,并找出它是如何指定的。

我们知道属性是在原型链中查找的:如果一个对象没有我们要读取的属性,我们就会沿着原型链向上查找,直到找到它(或找到不再具有原型的对象) 。

const o1 = { foo: 99 };
const o2 = {};
Object.setPrototypeOf(o2, o1);
o2.foo;// → 99

原型遍历是在哪里定义的?

让我们尝试找出这个行为是在哪里定义的。 一个好的起点是对象内部方法列表。

[[GetOwnProperty]][[Get]] - 我们对不限于对象本身的属性的遍历感兴趣,所以我们将选择 [[Get]]

不幸的是,属性描述符规范类型还有一个名为[[Get]] 的字段,因此在浏览 [[Get]] 规范时,我们需要仔细区分这两种独立的用法。

[[Get]]是一个重要的内部方法。 普通对象实现基本内部方法的默认行为。 异构对象可以定义它们自己的内部方法[[Get]],这与默认行为有所偏差。在这篇文章中,我们专注于普通对象。

[[Get]]的默认实现委托给OrdinaryGet

`[[Get]](P,Receiver)`
当调用O的[[Get]]内部方法,参数为属性键P和ECMAScript语言值Receiver时,执行以下步骤:
返回?OrdinaryGet(O,P,Receiver)。 

接下来,我们将看到Receiver是调用访问器属性的getter函数时用作this值的值。

OrdinaryGet定义如下:

OrdinaryGet(O,P,Receiver)
当调用抽象操作`OrdinaryGet`时,参数为对象O,属性键P和ECMAScript语言值Receiver时,执行以下步骤:
1. 断言:IsPropertyKey(P)为true。 
2. 让desc为?O.[[GetOwnProperty]](P)。 
3. 如果desc为undefined,则 
    a. 让parent为? O.[[GetPrototypeOf]]()。 
    b. 如果parent为null,则返回undefined。 
    c. 返回?parent.[[Get]](P, Receiver)。 
4. 如果IsDataDescriptor(desc)为true,则返回desc.[[Value]]。
5. 断言:IsAccessorDescriptor(desc)为true。 
6. 让getter为desc.[[Get]]。
7. 如果getter为undefined,则返回undefined。 
8. 返回?Call(getter,Receiver)。 

原型链遍历在第3步内:如果我们在自身属性中找不到属性,我们调用原型的[[Get]]方法,它再次委托给OrdinaryGet。如果我们仍然找不到属性,我们调用其原型的[[Get]]方法,它再次委托给OrdinaryGet,依此类推,直到我们找到属性或到达没有原型的对象。

让我们看看当我们访问o2.foo时,这个算法是如何工作的。首先,我们使用O为o2,P为"foo"调用OrdinaryGet。由于o2没有名为"foo"的自身属性,因此O.[[GetOwnProperty]]("foo")返回undefined,因此我们进入步骤3的if分支。在步骤3.a中,我们将parent设置为o2的原型,即o1。由于parent不为null,因此我们在步骤3.b中不返回。在步骤3.c中,我们使用属性键"foo"调用parent的[[Get]]方法,并返回它返回的任何内容。

父对象(o1)是普通对象,因此它的[[Get]]方法再次调用OrdinaryGet,这次使用O为o1,P为"foo"。o1有一个名为"foo"的自身属性,因此在步骤2中,O.[[GetOwnProperty]]("foo") 返回关联的属性描述符,并将其存储在desc中。

属性描述符是一种规范类型。数据属性描述符直接将属性的值存储在[[Value]]字段中。访问器属性描述符在[[Get]]和/或[[Set]]字段中存储访问器函数。在这种情况下,与"foo"关联的属性描述符是数据属性描述符。

在步骤2中我们存储在desc中的数据属性描述符不是undefined,因此我们不进入步骤3的if分支。接下来,我们执行步骤4。由于属性描述符是数据属性描述符,因此我们在步骤4中返回其[[Value]]字段,即99,随后完成了。

Receiver是什么,它从哪里来?

在访问器属性的情况下,Receiver参数仅在步骤8中使用。在调用访问器属性的getter函数时,它作为this值传递。

OrdinaryGet在整个递归过程中始终将原始Receiver传递,未更改(步骤3.c)。让我们找出Receiver最初来自哪里!

搜索[[Get]]被调用的地方,我们找到了一个名为GetValue的抽象操作,它操作引用(References)。引用是一种规范类型,由基值(base value)、引用的名称(referenced name)和严格引用标志(strict reference flag)组成。在o2.foo的情况下,基值是对象o2,引用的名称是字符串"foo",严格引用标志是false,因为示例代码是松散的。

另一个话题:为什么Reference不是Record?

另一个话题:Reference不是Record,尽管听起来它可能是。它包含三个组件,这三个组件同样可以表达为三个命名字段。Reference不是Record只是出于历史原因。

回到GetValue

让我们看看如何定义GetValue:

GetValue(V)
1. ReturnIfAbrupt(V)。 
2. 如果Type(V) 不是Reference,则返回V。 
3. 让base为GetBase(V)。 
4. 如果IsUnresolvableReference(V)为true,则抛出ReferenceError异常。 
5. 如果IsPropertyReference(V)为true,则 
    a. 如果HasPrimitiveBase(V)为true,则 
        1. 断言:在这种情况下,base永远不会是undefined或null。 
        2. 将base设置为!ToObject(base)。 
    b. 返回`? base.[[Get]](GetReferencedName(V), GetThisValue(V))`。
6. 否则, 
    1. 断言:base是环境记录。 
    2. 返回`? base.GetBindingValue(GetReferencedName(V), IsStrictReference(V))`

我们示例中的Reference是o2.foo,这是一个属性引用。因此,我们进入第5步。在步骤5.a中,我们不进入分支,因为基值(o2)不是原始值(Number、String、Symbol、BigInt、Boolean、Undefined或Null)。

然后我们在步骤5.b中调用[[Get]]。我们传递的Receiver是GetThisValue(V)。在这种情况下,它只是引用的基值:

GetThisValue(V)

1. 断言:IsPropertyReference(V) 为true。 
2. 如果IsSuperReference(V) 为true,则 
    1. 返回引用V的thisValue组件的值。 
3. 返回GetBase(V)。 

对于o2.foo,我们不进入步骤2的分支,因为它不是Super Reference(例如super.foo),但我们进行步骤3并返回Reference的基值,即o2。

将所有内容组合在一起,我们发现将Receiver设置为原始Reference的基值,然后在原型链遍历期间保持它不变。最后,如果找到的属性是访问器属性,我们在调用它时将Receiver用作this值。

特别是在getter中,this值指的是我们尝试获取属性的原始对象,而不是在原型链遍历期间找到属性的对象。

让我们尝试一下!

const o1 = { x: 10, get foo() { return this.x; } };
const o2 = { x: 50 };
Object.setPrototypeOf(o2, o1);
o2.foo;
// → 50

在这个例子中,我们有一个名为foo的访问器属性,并为其定义了一个getter。getter返回this.x。

然后我们访问o2.foo - getter会返回什么?

我们发现当我们调用getter时,this值是我们最初尝试获取属性的对象,而不是找到属性时的对象。在这种情况下,this值是o2,而不是o1。我们可以通过检查getter是否返回o2.x或o1.x来验证这一点,确实,它返回o2.x。

它起作用了!我们能够根据我们在规范中阅读到的内容来预测这段代码片段的行为。

访问属性 — 为什么调用[[Get]]

规范在哪里说当访问属性(例如o2.foo)时会调用Object内部方法[[Get]]?当然,这肯定是定义在某个地方的。别轻信我的话!

我们发现Object内部方法[[Get]]是从在引用上操作的抽象操作GetValue中调用的。但GetValue从何处调用?

MemberExpression的运行时语义

规范的语法规则定义了语言的语法结构。运行时语义定义了这些语法结构的“含义”(在运行时如何评估它们)。

如果您对无上下文文法不熟悉,现在是了解一下的好时机!

我们将在以后的篇章中深入研究语法规则,现在让我们保持简单!特别是,在这一集中,我们可以忽略语法结构的下标(Yield、Await等)。

以下产生描述了MemberExpression的外观:

MemberExpression :
  PrimaryExpression
  MemberExpression [ Expression ]
  MemberExpression . IdentifierName
  MemberExpression TemplateLiteral
  SuperProperty
  MetaProperty
  new MemberExpression Arguments

这里有7个MemberExpression的产生。MemberExpression可以只是一个PrimaryExpression。或者,可以通过将另一个MemberExpression和Expression组合在一起来构造MemberExpression:MemberExpression [ Expression ],例如o2['foo']。或者它可以是MemberExpression . IdentifierName,例如o2.foo — 这是我们例子中相关的产生。

生产MemberExpression : MemberExpression . IdentifierName的运行时语义定义了评估时要采取的步骤集:

运行时语义:对于MemberExpression : MemberExpression . IdentifierName的评估

1. 让baseReference为评估MemberExpression的结果。 
2. 让baseValue为? GetValue(baseReference)。 
3. 如果由此MemberExpression匹配的代码是严格模式代码,则让strict为true;否则,让strict为false。 
4. 返回?EvaluatePropertyAccessWithIdentifierKey(baseValue,IdentifierName,strict)。 

该算法委托给抽象操作EvaluatePropertyAccessWithIdentifierKey,因此我们也需要阅读它:

EvaluatePropertyAccessWithIdentifierKey(baseValue,identifierName,strict)

抽象操作EvaluatePropertyAccessWithIdentifierKey的参数是值baseValue、一个解析节点identifierName和一个布尔参数strict。它执行以下步骤:
1. 断言:identifierName是一个IdentifierName 
2. 让bv为`? RequireObjectCoercible(baseValue)`。 
3. 让propertyNameString为identifierName的StringValue。 
4. 返回一种Reference类型的值,其基值组件为bv,引用的名称组件为propertyNameString,且严格引用标志为strict。 

也就是说,EvaluatePropertyAccessWithIdentifierKey构造一个Reference,它使用提供的baseValue作为基值,identifierName的字符串值作为属性名,strict作为严格模式标志。

最终,这个Reference传递给GetValue。这在规范的几个地方定义,取决于Reference最终如何被使用。

MemberExpression作为参数

在我们的例子中,我们将属性访问用作参数:

console.log(o2.foo);

在这种情况下,行为定义在ArgumentList产生的运行时语义中,该语法规则在参数上调用GetValue:

运行时语义:ArgumentListEvaluation

ArgumentList:AssignmentExpression

1. 让ref为评估AssignmentExpression的结果。
2. 让arg为`? GetValue(ref)`。 
3. 返回一个仅包含arg的列表。 

o2.foo看起来不像AssignmentExpression,但它是一个,所以这个产生是适用的。要找出原因,您可以查看此额外内容,但在这一点上,这并不绝对必要。

步骤1中的AssignmentExpression是o2.foo。步骤1的ref,即评估o2.foo的结果,是上述提到的Reference。在步骤2中,我们对其调用GetValue。因此,我们知道Object内部方法[[Get]]将被调用,原型链遍历将发生。

总结

在本集中,我们看到规范如何跨所有不同的层面定义语言特性,本例中是原型查找:触发该特性的语法结构以及定义它的算法。

评论


0